//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: SimpleCacheManager.cs
//
//  Description: Implements a simple cache manager.
// 
//--------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using Chili.Opf3.Query;
using Chili.Opf3.Storages;
using System.Threading;
using Chili.Opf3.Mapping;

namespace Chili.Opf3.Caching
{
	/// <summary>
	/// Class that implements a simple caching algorithm.
	/// </summary>
	/// <remarks>
	/// <para>
	///		<strong>Thread Safety:</strong> This class is thread safe.
	/// </para>
	/// <para>
	/// This class implements a simple caching algorithm. This algorithm uses internally a weak
	/// references cache that holds instances of persistent objects. The instances are hold as long
	/// in the cache as long they exist in the current AppDomain (in simple words: in the current process). 
	/// If the instances have been collected by the GC they are also removed from this cache.
	/// </para>
	/// <para>
	/// To have persistent objects cached by the SimpleCacheManager they need to be decorated with the
	/// <see cref="SimpleCacheAttribute">SimpleCacheAttribute</see>. All persistent without that attribute, are
	/// not cached!
	/// </para>
	/// </remarks>
	/// <example>
	///     The following example shows how to set the SimpleCacheManager. 
	///     <code lang="CS">
	/// // Creates a new ObjectContext that uses an MsSql Server as storage.
	/// ObjectContext context = new ObjectContext(new MsSqlStorage("sa", "", 
	///     "localhost", "application"));
	/// // Set the CacheManager.
	/// SimpleCacheManager cache = new SimpleCacheManager();
	/// context.CacheManager = cache;
	/// // Extend the cache duration of a persistent if requested a second time.
	/// // Basically reset the expiration timer for that persistent.
	/// cache.ExtendDurationOnRequest = true;
	/// 
	/// // Set the default duration for a persistent to 10 seconds.
	/// cache.PersistentsDefaultDuration = new TimeSpan(10000);
	/// // Set the default duration for a query to 20 seconds.
	/// cache.QueriesDefaultDuration = new TimeSpan(20000);
	/// 
	/// // ... Other code.
	/// 
	/// // Clear all persistents that are cached.
	/// cache.ClearPersistents();
	/// 
	/// // Clear also all queries that are cached.
	/// cache.ClearQueries();
	///     </code>
	/// </example>
	public class SimpleCacheManager : CacheManagerBase
	{
		private readonly TimeSpan TIMEOUT = TimeSpan.FromSeconds(10);
		private readonly object LOCK = new object();
		private bool _fetchFromStorageIfNull;
		private TimeSpan _persistentsCacheDuration = TimeSpan.MaxValue;
		private TimeSpan _queriesCacheDuration = TimeSpan.MaxValue;
		private bool _extendDurationOnRequest;
		private bool _extendDurationOnMultipleAdd;
		private Thread _thread;
		private Dictionary<Type, SimpleCacheAttribute> _attributes = new Dictionary<Type, SimpleCacheAttribute>();

		private Dictionary<string, CachedPersistent> _persistentsCache = new Dictionary<string, CachedPersistent>();
		private Dictionary<string, CachedQuery> _queriesCache = new Dictionary<string, CachedQuery>();

		/// <summary>
		/// Creates a new instance of the <see cref="SimpleCacheManager">SimpleCacheManager Class</see>.
		/// </summary>
		public SimpleCacheManager()
		{
			_thread = new Thread(new ThreadStart(Collector));
			_thread.IsBackground = true;
			_thread.Start();
		}

		#region Collector thread

		/// <summary>
		/// Collects expired instances found in the cached.
		/// </summary>
		private void Collector()
		{
			while (true)
			{
				// Sleep for a while.
				Thread.Sleep(TIMEOUT);

				// Get a backup of all keys.
				List<string> keys = null;
				lock (LOCK)
				{
					keys = new List<string>(_persistentsCache.Keys);
				}

				// Loop backwards over the keys.
				for (int i = keys.Count - 1; i >= 0; i--)
				{
					CachedPersistent p = _persistentsCache[keys[i]];
					// If the persistent has expired remove it.
					if (p.Expired)
					{
						lock (LOCK)
						{
							_persistentsCache.Remove(keys[i]);
						}
					}
				}

				// Get the keys of the queries.
				keys.Clear();
				lock (LOCK)
				{
					keys.AddRange(_queriesCache.Keys);
				}

				// Loop backwards over the keys.
				for (int i = keys.Count - 1; i >= 0; i--)
				{
					CachedQuery query = _queriesCache[keys[i]];
					// If the query has expired remove it.
					if (query.Expired)
					{
						lock (LOCK)
						{
							_queriesCache.Remove(keys[i]);
						}
					}
				}
			}
		}

		#endregion

		#region Overridden Methods

		/// <summary>
		/// Gets an instance of the <see cref="ObjectReader{T}">ObjectReader</see> for the given query.
		/// </summary>
		/// <param name="context">The <see cref="ObjectContext">ObjectContext</see> used to load the reader.</param>
		/// <param name="query">A class that implements the <see cref="IQuery">IQuery</see> interface.</param>
		public override ObjectReader<T> GetObjectReader<T>(ObjectContext context, IQuery query)
		{
			// Get the attribute.
			SimpleCacheAttribute attribute = this.GetAttribute(typeof(T));
			if (attribute == null)
			{
				// Ignore the cache and return an object reader.
				return context.GetObjectReader<T>(query, true);
			}

			// Build the key for the query.
			string key = this.BuildUniqueKey(context.Storage, query);
			CachedQuery cachedQuery = null;

			// This section needs to be locked as only one thread should alter the
			// queries collection here.
			lock (LOCK)
			{
				// Try to get the query from the cache.
				if (_queriesCache.TryGetValue(key, out cachedQuery))
				{
					// Try to get the persistents for the query.
					IList<T> objects = null;
					if (cachedQuery.TryGetPersistents<T>(out objects))
					{
						if (objects.Count > 0 || !this.FetchFromStorageIfNull)
						{
							// Extend the time span if required.
							if (this.ExtendDurationOnRequest)
								cachedQuery.ValidUntil = this.GetValidUntil(attribute, false);

							// Return an ObjectReader with the result.
							return ObjectReader<T>.Create(context, objects);
						}
					}
				}
				// Otherwise remove the query from the cache.
				if (cachedQuery != null)
					_queriesCache.Remove(key);

				bool exitCancel = false;
				ObjectReader<T> reader = null;

				try
				{
					// Get the list of objects from the storage. Measure also the timespan for the query execute.
					DateTime start = DateTime.Now;
					reader = context.GetObjectReader<T>(query, true);
					DateTime end = DateTime.Now;
					
					// Get the event args for the AddingQuery event.
					AddingQueryEventArgs e = new AddingQueryEventArgs(query, context.Storage, this.GetValidUntil(attribute, false), (TimeSpan)(end - start));
					// Fire the event.
					OnAddingQuery(e);
					if (e.Cancel)
					{
						// Set the exit cancel to true and return the reader.
						exitCancel = true;
						return reader;
					}
					else
					{
						// If not to cancel add the items to the query.
						List<T> result = new List<T>();

						// Create a cached query.
						cachedQuery = new CachedQuery(query, context.Storage, e.ValidUntil, typeof(T));
						foreach (T obj in reader)
						{
							CachedPersistent persistent = null;
							// Add the object to the cache.
							this.AddObject(obj, out persistent);
							// Add the object to the query.
							cachedQuery.AddPersistent(persistent);

							// Add the persistent to the list.
							result.Add(obj);
						}

						// Add the query to the cache.
						_queriesCache.Add(key, cachedQuery);

						// Returns the object reader with the results.
						return ObjectReader<T>.Create(context, result);
					}
				}
				finally
				{
					// Dispose if not null and exitCancel is not true.
					if (reader != null && !exitCancel)
						reader.Dispose();
				}
			}
		}

		/// <summary>
		/// Adds a persistent object to the cache.
		/// </summary>
		/// <param name="obj">The object that is added.</param>
		public override bool AddPersistentObject(object obj)
		{
			CachedPersistent p = null;
			return AddObject(obj, out p);
		}

		#endregion

		/// <summary>
		/// Adds an object to the cache.
		/// </summary>
		/// <param name="obj">The object that is added.</param>
		/// <param name="persistent">The cached persistent object that is generated for the object.</param>
		private bool AddObject(object obj, out CachedPersistent persistent)
		{
			persistent = null;

			// Get the attribute for the object.
			SimpleCacheAttribute attribute = this.GetAttribute(obj.GetType());
			if (attribute == null)
				return false;

			// Build the key.
			string key = this.BuildUniqueKey(obj);

			lock (LOCK)
			{
				// Check if the cache contains the item.
				if (_persistentsCache.TryGetValue(key, out persistent))
				{
					if (!persistent.Expired)
					{
						// Extend the ttl if required.
						if (this.ExtendDurationOnMultipleAdd)
							persistent.ValidUntil = this.GetValidUntil(attribute, true);
						return false;
					}
					else
					{
						// Remove the persistent if no longer valid.
						_persistentsCache.Remove(key);
					}
				}

				// Invoke the adding persistent event.
				AddingPersistentEventArgs e = new AddingPersistentEventArgs(obj, this.GetValidUntil(attribute, true));
				OnAddingPersistent(e);
				// If to cancel, return.
				if (e.Cancel)
					return false;

				// Add the persistent to the cache.
				_persistentsCache.Add(key, new CachedPersistent(obj, e.ValidUntil));
				return true;
			}
		}

		/// <summary>
		/// Returns whether the given persistent object is in cache.
		/// </summary>
		/// <param name="obj">The persistent object that is checked for being in the cache.</param>
		public bool IsPersistentObjectInCache(object obj)
		{
			string key = this.BuildUniqueKey(obj);

			lock (LOCK)
			{
				return _persistentsCache.ContainsKey(key);
			}
		}

		/// <summary>
		/// Removes the persistent object from the cache.
		/// </summary>
		/// <param name="obj">The persistent object to remove.</param>
		public void RemovePersistentObject(object obj)
		{
			string key = this.BuildUniqueKey(obj);

			lock (LOCK)
			{
				_persistentsCache.Remove(key);
			}
		}

		/// <summary>
		/// Gets the valid until date time for the given attribute.
		/// </summary>
		/// <param name="attribute">The attribute that is used to create the date time.</param>
		/// <param name="persistent">Specifies whether the date time is created for a persistent or a query.</param>
		/// <returns></returns>
		private DateTime GetValidUntil(SimpleCacheAttribute attribute, bool persistent)
		{
			// Get the duration from the attribute.
			TimeSpan span = attribute.GetDurationAsTimeSpan();
			
			if (persistent)
			{
				// Use the default duration if lower or equal to -1.
				if (attribute.Duration <= -1)
					span = this.PersistentsDefaultDuration;
			}
			else
				span = this.QueriesDefaultDuration;

			// Return MaxValue if a maximum TimeSpan is specified. Otherwise add the span to the current date.
			return (span == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now.Add(span));
		}

		/// <summary>
		/// Gets the attribute for the given type or null if the type has no attribute.
		/// </summary>
		/// <param name="type">The type that is checked for the attribute.</param>
		/// <returns></returns>
		private SimpleCacheAttribute GetAttribute(Type type)
		{
			// Get the persistent type for the given type. Could be an interface.
			type = TypeMapping.GetPersistentType(type);

			SimpleCacheAttribute attribute = null;
			if (_attributes.TryGetValue(type, out attribute))
				return attribute;

			// Get all simplecacheattributes.
			object[] attributes = type.GetCustomAttributes(typeof(SimpleCacheAttribute), false);
			if (attributes.Length == 0)
				return null;

			// Get the first attribute.
			attribute = (SimpleCacheAttribute)attributes[0];

			// Add the attribute and return it.
			_attributes.Add(type, attribute);
			return attribute;
		}

		/// <summary>
		/// Gets or sets whether to fetch the persistent from storage if not found in cache.
		/// </summary>
		public bool FetchFromStorageIfNull
		{
			get { return _fetchFromStorageIfNull; }
			set { _fetchFromStorageIfNull = value; }
		}

		/// <summary>
		/// Gets or sets the default cache duration for a cached persistent.
		/// </summary>
		public TimeSpan PersistentsDefaultDuration
		{
			get { return _persistentsCacheDuration; }
			set { _persistentsCacheDuration = value; }
		}

		/// <summary>
		/// Gets or sets the default cache duration for a cached query.
		/// </summary>
		public TimeSpan QueriesDefaultDuration
		{
			get { return _queriesCacheDuration; }
			set { _queriesCacheDuration = value; }
		}

		/// <summary>
		/// Gets or sets whether to extend the cache duration on a cache request. If true each request of a persistent
		/// or query extends the cache duration for the specified time span.
		/// </summary>
		public bool ExtendDurationOnRequest
		{
			get { return _extendDurationOnRequest; }
			set { _extendDurationOnRequest = value; }
		}

		/// <summary>
		/// Gets or sets whether to extend the cache duration of an existing item if added a second time. If true 
		/// on a second add the cache duration of the existing object is extended.
		/// </summary>
		public bool ExtendDurationOnMultipleAdd
		{
			get { return _extendDurationOnMultipleAdd; }
			set { _extendDurationOnMultipleAdd = value; }
		}

		#region Events

		/// <summary>
		/// Signalizes that a query is being added to the cache.
		/// </summary>
		public event EventHandler<AddingQueryEventArgs> AddingQuery;

		/// <summary>
		/// Invokes the AddingQuery event.
		/// </summary>
		/// <param name="e"></param>
		private void OnAddingQuery(AddingQueryEventArgs e)
		{
			if (AddingQuery != null)
				AddingQuery(this, e);
		}

		/// <summary>
		/// Signalizes that a persistent is being added to the cache.
		/// </summary>
		public event EventHandler<AddingPersistentEventArgs> AddingPersistent;

		/// <summary>
		/// Invokes the AddingPersistent event.
		/// </summary>
		/// <param name="e"></param>
		private void OnAddingPersistent(AddingPersistentEventArgs e)
		{
			if (AddingPersistent != null)
				AddingPersistent(this, e);
		}

		#endregion

		#region Clear, ClearQueries, ClearQueriesOlderThan, ClearPersistentsOlderThan, ClearPersistents

		/// <summary>
		/// Clears the whole cache.
		/// </summary>
		public void Clear()
		{
			lock (LOCK)
			{
				_persistentsCache.Clear();
				_queriesCache.Clear();
			}
		}

		/// <summary>
		/// Clears a query for the given storage.
		/// </summary>
		/// <param name="storage">The storage the query has been executed on.</param>
		/// <param name="query">The query to clear.</param>
		public void ClearQuery(IStorage storage, IQuery query)
		{
			if (storage == null)
				throw new ArgumentNullException("storage");
			if (query == null)
				throw new ArgumentNullException("query");

			// Build the key for the query.
			string key = this.BuildUniqueKey(storage, query);

			// Remove it from the cache.
			_queriesCache.Remove(key);
		}

		/// <summary>
		/// Clears the queries cache.
		/// </summary>
		public void ClearQueries()
		{
			lock (LOCK)
			{
				_queriesCache.Clear();
			}
		}

		/// <summary>
		/// Clears all queries for the given storage.
		/// </summary>
		/// <param name="storage">The storage the query has been executed on.</param>
		public void ClearQueries(IStorage storage)
		{
			ClearQueriesOlderThan(storage, DateTime.Now);
		}

		/// <summary>
		/// Clears all queries for the given storage and persistent type.
		/// </summary>
		/// <param name="storage">The storage the query has been executed on.</param>
		/// <param name="persistentsType">The persistents' type returned by the query.</param>
		public void ClearQueries(IStorage storage, Type persistentsType)
		{
			if (storage == null)
				throw new ArgumentNullException("storage");
			if (persistentsType == null)
				throw new ArgumentNullException("persistentsType");

			// Backup the keys.
			List<string> keys = null;
			lock (LOCK)
			{
				keys = new List<string>(_queriesCache.Keys);
			}

			// Loop over the keys backwards.
			for (int i = keys.Count - 1; i >= 0; i--)
			{
				// Get the cached query.
				CachedQuery query = _queriesCache[keys[i]];
				// Check the storage.
				if (persistentsType == query.PersistentsType && query.Storage == storage)
				{
					// Remove the item if the storage equals.
					lock (LOCK)
					{
						_queriesCache.Remove(keys[i]);
					}
				}
			}
		}

		/// <summary>
		/// Clears all queries for the given storage.
		/// </summary>
		/// <param name="storage">The storage the query has been executed on.</param>
		/// <param name="date">The date all queries older than should be deleted.</param>
		public void ClearQueriesOlderThan(IStorage storage, DateTime date)
		{
			if (storage == null)
				throw new ArgumentNullException("storage");

			// Backup the keys.
			List<string> keys = null;
			lock (LOCK)
			{
				keys = new List<string>(_queriesCache.Keys);
			}

			// Loop over the keys backwards.
			for (int i = keys.Count - 1; i >= 0; i--)
			{
				// Get the cached query.
				CachedQuery query = _queriesCache[keys[i]];
				// Check the storage.
				if (date > query.CreatedOn && query.Storage == storage)
				{
					// Remove the item if the storage equals.
					lock (LOCK)
					{
						_queriesCache.Remove(keys[i]);
					}
				}
			}
		}

		/// <summary>
		/// Clears all persistents of the given type which are older then the given date.
		/// </summary>
		/// <param name="type">The type of persistents that are deleted.</param>
		/// <param name="date">The date all persistent objects older than should be deleted.</param>
		public void ClearPersistentsOlderThan(Type type, DateTime date)
		{
			// Backup the keys.
			List<string> keys = null;
			lock (LOCK)
			{
				keys = new List<string>(_persistentsCache.Keys);
			}

			// Loop over the keys backwards.
			for (int i = keys.Count - 1; i >= 0; i--)
			{
				// Get the cached persistent.
				CachedPersistent persistent = _persistentsCache[keys[i]];
				// Check the storage.
				if (date > persistent.CreatedOn && (type == null || persistent.Persistent.GetType() == type))
				{
					// Remove the item if the type equals.
					lock (LOCK)
					{
						_persistentsCache.Remove(keys[i]);
					}
				}
			}
		}

		/// <summary>
		/// Clears all which are older then the given date.
		/// </summary>
		/// <param name="date">The date all persistent objects older than should be deleted.</param>
		public void ClearPersistentsOlderThan(DateTime date)
		{
			ClearPersistentsOlderThan(null, date);
		}

		/// <summary>
		/// Clears all persistent objects in the cache.
		/// </summary>
		public void ClearPersistents()
		{
			lock (LOCK)
			{
				_persistentsCache.Clear();
			}
		}

		/// <summary>
		/// Clears all persistent objects of the given type.
		/// </summary>
		/// <param name="type">The type of persistents that are deleted.</param>
		public void ClearPersistents(Type type)
		{
			if (type == null)
				throw new ArgumentNullException("type");

			ClearPersistentsOlderThan(type, DateTime.Now);
		}

		#endregion
	}
}
